package org.kingtop.mybatis.plugin;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.kingtop.mybatis.plugin.parser.ISqlOptimizer;
import org.kingtop.mybatis.plugin.parser.TotalSqlOptimizer;
import org.kingtop.mybatis.util.JdbcUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;

/**
 * 缓存分页拦截器,解决了分页存在缓存的问题
 * 
 * @author 陈金波
 * @date 2018年1月7日 上午11:01:57
 * @Copyright: 2018 chen jinbo. All rights reserved.
 */
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
		RowBounds.class, ResultHandler.class }) })
public class PaginationExecutorInterceptor implements Interceptor {

	private Logger logger = LoggerFactory.getLogger(getClass());

	private final static String SQL_BASE_COUNT = "SELECT COUNT(1) FROM ( %s ) TOTAL";
	/** COUNT SQL 解析*/
	private ISqlOptimizer sqlParser = new TotalSqlOptimizer();
	/** 溢出总页数，设置最后一页 */
	private boolean overflowCurrent = false;
	/** 方言实现类 */
	private String dialectClazz;

	/** 数据库类型 **/
	private String dbType = null;

	/**
	 * Physical Pagination Interceptor for all the queries with parameter
	 * {@link org.apache.ibatis.session.RowBounds}
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {

		Object[] args = invocation.getArgs();
		RowBounds rowBounds = (RowBounds) args[2];
		if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
			// 无需分页
			return invocation.proceed();
		}

		Executor executor = (Executor) invocation.getTarget();
		Connection connection = executor.getTransaction().getConnection();
		Object parameterObject = invocation.getArgs()[1];
		MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
		BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
		String originalSql = boundSql.getSql();
		// 获取数据库类型
		dbType = StringUtils.isNotBlank(dbType) ? dbType : JdbcUtils.getDbType(connection.getMetaData().getURL());
		// 判断是否为自定义的分页
		if (rowBounds instanceof Pagination) {
			Pagination page = (Pagination) rowBounds;
			if (page.isSearchCount()) {
				String totalSql = originalSql;
				// 优化统计总条数的SQL语句
				if (sqlParser != null) {
					totalSql = sqlParser.optimizeSql(totalSql);
				}
				totalSql = String.format(SQL_BASE_COUNT, totalSql);
				logger.info(totalSql);
				this.queryTotal(overflowCurrent, totalSql, mappedStatement, boundSql, page, connection);
				if (page.getTotal() <= 0) {
					return invocation.proceed();
				}
			}
		}

		// 重新构建分页语句
		originalSql = DialectFactory.buildPageSql(rowBounds.getOffset(), rowBounds.getLimit(), originalSql, dbType,
				dialectClazz);

		// 这里的SqlSource不能使用RawSqlSource，因为这样会丢失BoundSql在的参数boundSql.getParameterMappings()，因此采取自己实现接口，大坑
		// RawSqlSource的实现中，是通过new HashMap<String, Object>()去重新匹配参数关系的
		SqlSource sqlSource = new StaticSqlSource(mappedStatement.getConfiguration(), originalSql,
				boundSql.getParameterMappings());
		MappedStatement newMs = copyFromMappedStatement(mappedStatement, sqlSource);
		args[0] = newMs;
		/*
		 * <p> 禁用内存分页 </p> <p> 内存分页会查询所有结果出来处理（这个很吓人的），如果结果变化频繁这个数据还会不准。</p>
		 */
		args[2] = RowBounds.DEFAULT;

		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		if (target instanceof Executor) {
			return Plugin.wrap(target, this);
		}
		return target;
	}

	@Override
	public void setProperties(Properties prop) {
		String dialectType = prop.getProperty("dbType");
		String dialectClazz = prop.getProperty("dialectClazz");

		if (!StringUtils.isEmpty(dialectType)) {
			// 去除空格
			this.dbType = StringUtils.trim(dialectType);
		}
		if (!StringUtils.isEmpty(dialectClazz)) {
			this.dialectClazz = dialectClazz;
		}
	}

	/**
	 * 查询总记录条数
	 *
	 * @param sql
	 * @param mappedStatement
	 * @param boundSql
	 * @param page
	 */
	protected void queryTotal(boolean overflowCurrent, String sql, MappedStatement mappedStatement, BoundSql boundSql,
			Pagination page, Connection connection) {
		try (PreparedStatement statement = connection.prepareStatement(sql)) {
			// setParameters(mappedStatement, boundSql.getParameterObject(), boundSql,
			// statement);
			int total = 0;
			BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(),
					boundSql.getParameterObject());
			ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,
					boundSql.getParameterObject(), countBS);
			parameterHandler.setParameters(statement);
			try (ResultSet resultSet = statement.executeQuery()) {
				if (resultSet.next()) {
					total = resultSet.getInt(1);
				}
			}
			page.setTotal(total);
			/*
			 * 溢出总页数，设置第一页
			 */
			int pages = page.getPages();
			if (overflowCurrent && (page.getCurrent() > pages)) {
				// 设置为最后一页
				page.setCurrent(pages);
			}
		} catch (Exception e) {
			logger.error("Error: Method queryTotal execution error !", e);
		}
	}

	/**
	 * 为分页语句重新构建 MappedStatement
	 * 
	 * @author 陈金波
	 * @date 2018年1月7日 上午10:58:37
	 * @param ms
	 * @param newSqlSource
	 * @return
	 * @return MappedStatement
	 */
	private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
		MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource,
				ms.getSqlCommandType());

		builder.resource(ms.getResource());
		builder.fetchSize(ms.getFetchSize());
		builder.statementType(ms.getStatementType());
		builder.keyGenerator(ms.getKeyGenerator());
		if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
			builder.keyProperty(StringUtils.join(ms.getKeyProperties(), ","));
		}
		if (ms.getKeyColumns() != null && ms.getKeyColumns().length > 0) {
			builder.keyColumn(StringUtils.join(ms.getKeyColumns(), ","));
		}
		builder.timeout(ms.getTimeout());
		builder.parameterMap(ms.getParameterMap());
		builder.resultMaps(ms.getResultMaps());
		builder.resultSetType(ms.getResultSetType());
		if (ms.getResultSets() != null && ms.getResultSets().length > 0) {
			builder.resultSets(StringUtils.join(ms.getResultSets(), ","));
		}
		builder.resultOrdered(ms.isResultOrdered());
		builder.cache(ms.getCache());
		builder.flushCacheRequired(ms.isFlushCacheRequired());
		builder.useCache(ms.isUseCache());
		builder.databaseId(ms.getDatabaseId());

		return builder.build();
	}

	/**
	 * @return the sqlParser
	 */
	public ISqlOptimizer getSqlParser() {
		return sqlParser;
	}

	/**
	 * @param sqlParser
	 *            the sqlParser to set
	 */
	public void setSqlParser(ISqlOptimizer sqlParser) {
		this.sqlParser = sqlParser;
	}

	/**
	 * @return the overflowCurrent
	 */
	public boolean isOverflowCurrent() {
		return overflowCurrent;
	}

	/**
	 * @param overflowCurrent
	 *            the overflowCurrent to set
	 */
	public void setOverflowCurrent(boolean overflowCurrent) {
		this.overflowCurrent = overflowCurrent;
	}

	/**
	 * @return the dialectClazz
	 */
	public String getDialectClazz() {
		return dialectClazz;
	}

	/**
	 * @param dialectClazz
	 *            the dialectClazz to set
	 */
	public void setDialectClazz(String dialectClazz) {
		this.dialectClazz = dialectClazz;
	}

	/**
	 * @return the dbType
	 */
	public String getDbType() {
		return dbType;
	}

	/**
	 * @param dbType
	 *            the dbType to set
	 */
	public void setDbType(String dbType) {
		this.dbType = dbType;
	}

}
